/*
 * Copyright 2012 Gene Chen
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package ubc.cs.cpsc210.sustainabilityapp;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.mapsforge.android.maps.ArrayCircleOverlay;
import org.mapsforge.android.maps.ArrayItemizedOverlay;
import org.mapsforge.android.maps.ArrayWayOverlay;
import org.mapsforge.android.maps.GeoPoint;
import org.mapsforge.android.maps.MapActivity;
import org.mapsforge.android.maps.MapController;
import org.mapsforge.android.maps.MapView;
import org.mapsforge.android.maps.MapViewMode;
import org.mapsforge.android.maps.OverlayCircle;
import org.mapsforge.android.maps.OverlayItem;
import org.mapsforge.android.maps.OverlayWay;

import ubc.cs.cpsc210.sustainabilityapp.model.POIRegistry;
import ubc.cs.cpsc210.sustainabilityapp.model.PointOfInterest;
import ubc.cs.cpsc210.sustainabilityapp.model.SharedPreferencesKeyValueStore;
import ubc.cs.cpsc210.sustainabilityapp.model.TourState;
import ubc.cs.cpsc210.sustainabilityapp.routing.RouteInfo;
import ubc.cs.cpsc210.sustainabilityapp.routing.RoutingService;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Color;
import android.graphics.Paint;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import com.google.zxing.integration.android.IntentIntegrator;
import com.google.zxing.integration.android.IntentResult;

/**
 * Activity representing the Map tab in the UI.
 */
public class MapDisplayActivity extends MapActivity {
	private static final int DARK_BLUE = Color.rgb(0x57, 0x81, 0xFC);

	private final static String LOG_TAG = "MapDisplayActivity";

	/**
	 * Overlay for POI markers.
	 */
	private ArrayItemizedOverlay poiOverlay;

	/**
	 * Overlay for the user's current location.
	 */
	private ArrayItemizedOverlay myLocationOverlay;

	private ArrayCircleOverlay myUncertOverlay;

	/**
	 * Overlay for the route connecting the selected POI's. 
	 */
	private ArrayWayOverlay tourOverlay;

	/**
	 * Overlay for the route connecting the user's current location to the nearest 
	 * selected POI.
	 */
	private ArrayWayOverlay routeToTourOverlay;

	/**
	 * Manages and stores selected features and POI's.
	 */
	private TourState tourState;

	/**
	 * Currently selected POI's.
	 */
	private List<PointOfInterest> selectedPOIs;

	/**
	 * Android location manager (tracks current location of device).
	 */
	private LocationManager locationManager;

	/**
	 * Wrapper for a service which calculates routes between POI's, and between the user's current location and the
	 * nearest selected POI.
	 */
	private RoutingService routingService;

    private MapController mapController;

	private Location lastLoc;
	
	private MapView mapView;

	private ARView overlay;

	private SensorManager sensorManager;

	private Sensor orientationSensor;
	
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.mapview);
		
		tourState = new TourState(POIRegistry.getDefault(), 
				new SharedPreferencesKeyValueStore(this, TourState.STORE_NAME));

		routingService = new RoutingService();

        mapView = (MapView)findViewById(R.id.mapview);
        overlay = (ARView)findViewById(R.id.overlay);
		// Maps are downloaded from the Mapnik server -- an internet connection is required.
		mapView.setMapViewMode(MapViewMode.MAPNIK_TILE_DOWNLOAD);
		mapView.setMemoryCardCachePersistence(true);
		mapView.setClickable(true);
		mapView.setBuiltInZoomControls(true);

		mapController = mapView.getController();

		// With the Mapnik server, this zoom level results in a map which encompasses most of
		// the UBC campus.
		mapController.setZoom(mapView.getMaxZoomLevel() - 4);
		
		poiOverlay = createPOIOverlay();
		myLocationOverlay = createMyLocationOverlay();
		myUncertOverlay = createMyUncertOverlay();
		tourOverlay = createTourOverlay();
		routeToTourOverlay = createRouteToTourOverlay();

		// Order matters: overlays added later are displayed on top of overlays added earlier.
		mapView.getOverlays().add(myUncertOverlay);
		mapView.getOverlays().add(routeToTourOverlay);
		mapView.getOverlays().add(tourOverlay);
		mapView.getOverlays().add(poiOverlay);
		mapView.getOverlays().add(myLocationOverlay);

		locationManager = (LocationManager)getSystemService(Context.LOCATION_SERVICE);
		sensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
		orientationSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);

		lastLoc = getLastLocation(49.260887, -123.24902);

		// Center the map on the last known location
		GeoPoint point = new GeoPoint(lastLoc.getLatitude(), lastLoc.getLongitude());
		mapController.setCenter(point);
	}

	@Override 
	protected void onDestroy() {
		if(isFinishing() && routingService != null) {
			routingService.shutdown();
		}
		super.onDestroy();
	}

	/**
	 * Update the overlays based on the selected POI's and the user's current location.
	 */
	@Override
	protected void onResume() {
		super.onResume();
		
		selectedPOIs = tourState.getSelectedPOIs();
		updateTour(selectedPOIs);
		overlay.updateTour(lastLoc, selectedPOIs);

		lastLoc = getLastLocation(49.260887, -123.24902);
		updateMyLocation(lastLoc, selectedPOIs);
		
		locationManager.requestLocationUpdates(
				LocationManager.GPS_PROVIDER, 0, 5, locationListener);
		
		sensorManager.registerListener(orientationEventListener, orientationSensor, SensorManager.SENSOR_DELAY_UI);
	}

	@Override
	protected void onPause() {
		super.onPause();
		
		locationManager.removeUpdates(locationListener);
		sensorManager.unregisterListener(orientationEventListener);
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		MenuInflater inflater = getMenuInflater();
		inflater.inflate(R.menu.menu, menu);

		return super.onCreateOptionsMenu(menu);
	}
	
	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		// Handle item selection
		switch (item.getItemId()) {
		case R.id.currentloc:
			Location lastLoc = getLastLocation();
			GeoPoint point = new GeoPoint(lastLoc.getLatitude(), lastLoc.getLongitude());
			mapController.setCenter(point);
			return true;
		case R.id.scanqr:
			IntentIntegrator.initiateScan(this);
			return true;
		case R.id.selectpois:
			Intent intent = new Intent();
			intent.setClass(this, SelectPOIActivity.class);
			startActivityForResult(intent, 0);
			return true;
		default:
			return super.onOptionsItemSelected(item);
		}
	}
	
	@Override
	public void onConfigurationChanged(Configuration newConfig) {
		super.onConfigurationChanged(newConfig);
	}

	@Override
	public void onActivityResult(int requestCode, int resultCode, Intent intent) {
		super.onActivityResult(requestCode, resultCode, intent);
		
		IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent);

		if(scanResult != null) {
			String contents = scanResult.getContents();

			if(contents != null) {
				if(contents.startsWith("http://") || contents.startsWith("https://")) {
					Intent i = new Intent(Intent.ACTION_VIEW);
					i.setData(Uri.parse(contents));
					startActivity(i);
				}
			}
		}
	}
	
	@Override
	public void onBackPressed() {
		Location lastLoc = getLastLocation(49.260887, -123.24902);
		GeoPoint point = new GeoPoint(lastLoc.getLatitude(), lastLoc.getLongitude());
		mapController.setCenter(point);
	}
	
	private Location getLastLocation() {
		LocationManager lm = (LocationManager) getSystemService(Context.LOCATION_SERVICE);  
		List<String> providers = lm.getProviders(true);

		/* Loop over the array backwards, and if you get an accurate location, then break out the loop*/
		Location l = null;

		for (int i=providers.size()-1; i>=0; i--) {
			l = lm.getLastKnownLocation(providers.get(i));
			if (l != null) break;
		}
	
		return l;
	}
	
	private Location getLastLocation(double defaultLat, double defaultLong) {
		Location l = getLastLocation();
		
		if(l == null) {
			l = new Location("");
			l.setLatitude(defaultLat);
			l.setLongitude(defaultLong);
		}

		return l;
	}
	
	/**
	 * Update the POI markers and the route connecting them.
	 */
	private void updateTour(List<PointOfInterest> pois) {
		poiOverlay.clear();
		if (pois != null) {
			for (PointOfInterest poi: pois) {
				plotPOI(poiOverlay, poi);
			}
		}

		tourOverlay.clear();

		if (pois != null && pois.size() > 1) {
			List<Location> pointsOnTour = new ArrayList<Location>();
			for (PointOfInterest poi: pois) {
				pointsOnTour.add(poi.getLocation());
			}
			// Make sure the route connects back to the starting point.
			pointsOnTour.add(pois.get(0).getLocation());

			findRouteAndUpdateOverlay(tourOverlay, pointsOnTour, true);
		} 

	}
	
	/**
	 * Update the marker for the user's location, and the route connecting the user's location
	 * to the nearest selected POI.
	 */
	private void updateMyLocation(Location location, List<PointOfInterest> pois) {			
		lastLoc = location;
		myLocationOverlay.clear();
		myUncertOverlay.clear();

		plotLocation(myLocationOverlay, location, "Your Current Location");
		plotUncert(myUncertOverlay, location, "Location Uncertainty");

		routeToTourOverlay.clear();
		if(pois != null && pois.size() > 0) {
			PointOfInterest closest = findClosestPOI(location, pois);
			List<Location> latLongs = Arrays.asList(location, closest.getLocation());
			// Do not cache the route, since user's location constantly changes.
			findRouteAndUpdateOverlay(routeToTourOverlay, latLongs, false);
		} 
	}

	/**
	 * Plot a POI on the specified overlay.
	 */
	private void plotPOI(ArrayItemizedOverlay overlay, PointOfInterest poi) {
		GeoPoint point = new GeoPoint(poi.getLocation().getLatitude(), poi.getLocation().getLongitude());
		OverlayItem overlayItem = new OverlayItem(point, poi.getDisplayName(), poi.getDescription());
		overlay.addItem(overlayItem);
	}

	/**
	 * Plot an Android location on the specified overlay.
	 */
	private void plotLocation(ArrayItemizedOverlay overlay, Location location, String title) {
		GeoPoint point = new GeoPoint(location.getLatitude(), location.getLongitude());
		OverlayItem overlayItem = new OverlayItem(point,title, null);
		overlay.addItem(overlayItem);
	}

	private void plotUncert(ArrayCircleOverlay overlay, Location location, String title) {
		GeoPoint point = new GeoPoint(location.getLatitude(), location.getLongitude());
		OverlayCircle circle = new OverlayCircle(point, location.getAccuracy(), title);
		overlay.addCircle(circle);
	}

	/** 
	 * Given a location and a list of POI's, find the POI closest to the specified location.
	 * 
	 * This is based on "line-of-sight" distance between points, using an approximation which works
	 * okay for short distances (surface of the earth is approximated by a plane).
	 */
	private PointOfInterest findClosestPOI(Location location, List<PointOfInterest> pois) {
		PointOfInterest closest = null;
		double minDistValue = Double.MAX_VALUE;

		for (PointOfInterest poi: pois) {
			double distValue = location.distanceTo(poi.getLocation());
			if (distValue < minDistValue) {
				minDistValue = distValue;
				closest = poi;
			}
		}

		return closest;
	}

	/**
	 * Create the overlay for POI markers.
	 */
	private ArrayItemizedOverlay createPOIOverlay() {
		return new ArrayItemizedOverlay(getResources().getDrawable(R.drawable.map_pin_blue)) {
			@Override
			protected boolean onTap(int index) {
				PointOfInterest poi = selectedPOIs.get(index);
				AlertDialog.Builder dialog = new AlertDialog.Builder(MapDisplayActivity.this);
				dialog.setTitle(poi.getDisplayName());
				dialog.setMessage(poi.getDescription());
				dialog.show();
				return true;
			}
		};
	}

	/**
	 * Create the overlay for the user's current location.
	 */
	private ArrayItemizedOverlay createMyLocationOverlay() {
		return new ArrayItemizedOverlay(getResources().getDrawable(R.drawable.map_pin_walk));
	}

	private ArrayCircleOverlay createMyUncertOverlay() {
		Paint circleOverlayFill = new Paint(Paint.ANTI_ALIAS_FLAG);
		circleOverlayFill.setColor(DARK_BLUE);
		circleOverlayFill.setAlpha(50);

		Paint circleOverlayOutline = new Paint(Paint.ANTI_ALIAS_FLAG);
		circleOverlayOutline.setStyle(Paint.Style.STROKE);
		circleOverlayOutline.setColor(DARK_BLUE);
		circleOverlayOutline.setAlpha(150);

		return new ArrayCircleOverlay(circleOverlayFill, circleOverlayOutline);
	}

	/**
	 * Create the overlay for the route connecting the selected POI's.
	 */
	private ArrayWayOverlay createTourOverlay() {
		Paint wayOverlayFill = new Paint(Paint.ANTI_ALIAS_FLAG);
		wayOverlayFill.setStyle(Paint.Style.STROKE);
		wayOverlayFill.setColor(DARK_BLUE);
		wayOverlayFill.setStrokeWidth(4);

		return new ArrayWayOverlay(wayOverlayFill, null);
	}

	/**
	 * Create the overlay connecting the user's current location to the closest selected POI.
	 */
	private ArrayWayOverlay createRouteToTourOverlay() {
		Paint wayOverlayFill = new Paint(Paint.ANTI_ALIAS_FLAG);
		wayOverlayFill.setStyle(Paint.Style.STROKE);
		wayOverlayFill.setColor(DARK_BLUE);
		wayOverlayFill.setStrokeWidth(4);

		return new ArrayWayOverlay(wayOverlayFill, null);
	}

	/**
	 * Calls the routing service to obtain a route which connects the specified list of lat/long points.
	 * 
	 * @param overlay The way overlay which will be updated with the resulting route.
	 * @param points Points which the route must pass through.
	 * @param useCache If set to true, the routing service will return a cached route if one is available 
	 *                 (and will cache the result if no cached route is found).
	 */
	private void findRouteAndUpdateOverlay(ArrayWayOverlay overlay, List<Location> points, boolean useCache) {
		// Retrieve routes in a separate thread, as it can take some time and we do not want to 
		// block the UI thread.
		RouteRetriever retriever = new RouteRetriever(overlay, points, useCache);
		new Thread(retriever).start();
	}

	/**
	 * Add a route to the specified overlay.
	 */
	private void addRouteToOverlay(ArrayWayOverlay overlay, List<Location> waypoints) {
		List<GeoPoint> points = new ArrayList<GeoPoint>();
		for (Location latLong: waypoints) {
			points.add(new GeoPoint(latLong.getLatitude(), latLong.getLongitude()));
		}
		overlay.addWay(new OverlayWay(new GeoPoint[][]{points.toArray(new GeoPoint[]{})}));
	}

	/**
	 * Calls the routing service to obtain a route which connects the specified list of lat/long points,
	 * and updates the overlay provided with the resulting route.
	 * 
	 * Routes are retrieved in a separate thread, as it can take some time and we do not want to 
	 * block the UI thread.
	 */
	private class RouteRetriever implements Runnable {
		private ArrayWayOverlay overlay;
		private List<Location> points;
		private boolean useCache;

		public RouteRetriever(ArrayWayOverlay overlay, List<Location> points, boolean useCache) {
			this.overlay = overlay;
			this.points = points;
			this.useCache = useCache;
		}

		@Override
		public void run() {
			try {
				if (points.size() > 1) {
					for (int i = 1; i < points.size(); i++) {
						Location currPoint = points.get(i-1);
						Location nextPoint = points.get(i);
						RouteInfo info = routingService.getRoute(currPoint, nextPoint, useCache);
						
						if(info == null)
							return;

						final List<Location> waypoints = new ArrayList<Location>();
						waypoints.add(currPoint);
						waypoints.addAll(info.getWaypoints());
						waypoints.add(nextPoint);

						// Updates to the UI must run on the UI thread.
						runOnUiThread(new Runnable() {

							@Override
							public void run() {
								addRouteToOverlay(overlay, waypoints);
							}

						});
					}
				}
			} catch (Exception e) { 
				Log.e(LOG_TAG, "Error retrieving route from route service", e);
			}
		}
	}
	
	
	private final SensorEventListener orientationEventListener = new SensorEventListener() {

		@Override
		public void onAccuracyChanged(Sensor sensor, int accuracy) { }

		@Override
		public void onSensorChanged(SensorEvent event) {

			if (event.accuracy == SensorManager.SENSOR_STATUS_UNRELIABLE)
				return;	
			
		    overlay.orientationChanged(event.values[0]);		    
		}

	};

	/**
	 * Implement the LocationListener interface to receive updates from Android when the device's
	 * location changes.
	 */
	private final LocationListener locationListener = new LocationListener() {

		@Override
		public void onLocationChanged(Location location) {
			if(location != null) {
				updateMyLocation(location, selectedPOIs);
				overlay.updateTour(location, tourState.getSelectedPOIs());
			}
		}

		@Override
		public void onProviderDisabled(String provider) {
			// do nothing
		}

		@Override
		public void onProviderEnabled(String provider) {
			// do nothing
		}

		@Override
		public void onStatusChanged(String provider, int status, Bundle extras) {
			// do nothing
		}

	};

}
